package mysql

import (
	"errors"
	"fmt"
	"gitee.com/lstack_light/light-utils/middleware/sql/common"
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
	"gorm.io/gorm/logger"
	"log"
	"os"
	"reflect"
	"time"
)

const (
	notFound = "record not found"
)

// IgnoreNotFoundErrors
/*

@author KangXu

@description Ignored error indicating that no data can be queried

@param err indicates the error information generated during the communication with the database

@return Returns information other than the error caused by the query failure
*/
func ignoreNotFoundErrors(err error) error {
	if nil == err || err.Error() == notFound {
		return nil
	}
	return err
}

// AssembleQueryConditions
/*

@author KangXu

@description This method is used to assemble the query criteria

@param query is the condition for the update

@return Returns a string of all keys and an array of all values in the query
*/
func assembleQueryConditions(query map[string]interface{}) (string, []interface{}) {
	keys := ""
	values := make([]interface{}, 0)
	for key, value := range query {
		keys += key + " = ? AND "
		values = append(values, value)
	}
	keys = keys[0 : len(keys)-5]
	return keys, values
}

type client struct {
	Db *gorm.DB
}

type DbConfig struct {
	UserName        string
	Password        string
	Address         string
	Port            int
	DataBase        string
	ConnMaxLifetime time.Duration
	MaxOpenCons     int
	MaxIdleCons     int
	Prefix          string
	LogMode         logger.LogLevel
}

type native struct {
	client
}

// CreateMysqlCustomClient
/*

@author KangXu

@description Example Initialize the native mysql client

@return Returns a native mysql client pointer object
*/
func (s *client) createMysqlCustomClient() *native {
	return &native{client{s.Db}}
}

type Query interface {
	Count(arg string, alias ...string) *query
	Sum(arg string, alias ...string) *query
	Avg(arg string, alias ...string) *query
	Query(arg map[string]string) *query
}

var DefaultQuery Query = &query{}

type query struct {
	Key   string
	Alias string
}

func (q *query) Count(arg string, alias ...string) *query {
	switch len(q.Key) {
	case 0:
		q.Key += "COUNT(" + arg + ")"
	default:
		q.Key += ", COUNT(" + arg + ")"
	}
	if 1 == len(alias) {
		q.Key += " " + alias[0]
	}
	return q
}

func (q *query) Sum(arg string, alias ...string) *query {
	switch len(q.Key) {
	case 0:
		q.Key += "SUM(" + arg + ")"
	default:
		q.Key += ", SUM(" + arg + ")"
	}
	if 1 == len(alias) {
		q.Key += " " + alias[0]
	}
	return q
}

func (q *query) Avg(arg string, alias ...string) *query {
	switch len(q.Key) {
	case 0:
		q.Key += "AVG(" + arg + ")"
	default:
		q.Key += ", AVG(" + arg + ")"
	}
	if 1 == len(alias) {
		q.Key += " " + alias[0]
	}
	return q
}

func (q *query) Query(arg map[string]string) *query {
	if 0 < len(q.Key) {
		q.Key += ", "
	}
	for key, alias := range arg {
		q.Key += key + " " + alias + ", "
	}
	q.Key = q.Key[0 : len(q.Key)-2]
	return q
}

type Native interface {
	GetType() common.DbType
	Select(query *query) *native
	From(table string, alias ...interface{}) *native
	Where(arg string, value interface{}) *native
	LeftJoin(table, leftField, rightField string) *native
	RightJoin(table, leftField, rightField string) *native
	And(arg map[string]interface{}) *native
	Or(arg map[string]interface{}) *native
	GroupBy(arg string) *native
	OrderBy(arg map[string]string) *native
	Limit(page, pageSize int) *native
	Result(result interface{}) error
}

func (n *native) GetType() common.DbType {
	return common.Mysql
}

func (n *native) Select(query *query) *native {
	n.Db = n.Db.Select(query.Key + " " + query.Alias)
	return n
}

func (n *native) From(table string, alias ...interface{}) *native {
	n.Db = n.Db.Table(table, alias...)
	return n
}

func (n *native) Where(arg string, value interface{}) *native {
	n.Db = n.Db.Where(arg+" = ?", value)
	return n
}

func (n *native) LeftJoin(table, leftField, rightField string) *native {
	n.Db = n.Db.Joins(" LEFT JOIN " + table + " ON " + leftField + " = " + rightField)
	return n
}

func (n *native) RightJoin(table, leftField, rightField string) *native {
	n.Db = n.Db.Joins(" RIGHT JOIN " + table + " ON " + leftField + " = " + rightField)
	return n
}

func (n *native) And(arg map[string]interface{}) *native {
	keys, values := assembleQueryConditions(arg)
	n.Db = n.Db.Where(keys, values...)
	return n
}

func (n *native) Or(arg map[string]interface{}) *native {
	keys, values := assembleQueryConditions(arg)
	n.Db = n.Db.Or(keys, values...)
	return n
}

func (n *native) GroupBy(arg string) *native {
	n.Db = n.Db.Group(arg)
	return n
}

func (n *native) OrderBy(arg map[string]string) *native {
	sort := ""
	for field, rules := range arg {
		sort += field + " " + rules + ", "
	}
	sort = sort[0 : len(sort)-2]
	n.Db = n.Db.Order(sort)
	return n
}

func (n *native) Limit(page, pageSize int) *native {
	n.Db = n.Db.Offset(page).Limit(pageSize)
	return n
}

func (n *native) Result(result interface{}) error {
	return n.Db.Scan(result).Error
}

// NewMysqlClient
/*

@author KangXu

@description Initialize a mysql client

@param dbConfig is used to initialize the mysql database

@return Return the pointer object of the mysql client and the pointer object of the native concatenation client and the error information generated during the process
*/
func NewMysqlClient(dbConfig *DbConfig) (*client, *native, error) {
	// Joining together the url
	url := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local",
		dbConfig.UserName, dbConfig.Password, dbConfig.Address, dbConfig.Port, dbConfig.DataBase)
	db, err := gorm.Open(mysql.Open(url), &gorm.Config{})
	if nil != err {
		return nil, nil, err
	}
	// Start the Logger to display detailed logs
	db.Logger = logger.New(log.New(os.Stdout, dbConfig.Prefix, log.LstdFlags), logger.Config{
		SlowThreshold:             200 * time.Millisecond,
		LogLevel:                  dbConfig.LogMode,
		IgnoreRecordNotFoundError: false,
		Colorful:                  true,
	})
	sqlClient, sqlErr := db.DB()
	if nil != sqlErr {
		return nil, nil, sqlErr
	}
	// Set connection pool properties
	sqlClient.SetConnMaxLifetime(dbConfig.ConnMaxLifetime * time.Minute)
	sqlClient.SetMaxOpenConns(dbConfig.MaxOpenCons)
	sqlClient.SetMaxIdleConns(dbConfig.MaxIdleCons)
	s := client{Db: db}
	return &s, s.createMysqlCustomClient(), nil
}

// Exit
/*

@author KangXu

@description Disconnect the mysql database

@return Return a possible error message
*/
func (s *client) Exit() error {
	db, err := s.Db.DB()
	if nil != err {
		return err
	}
	return db.Close()
}

// Migrate
/*

@author KangXu

@description Automatically migrate table structures

@param tables is the name of the table you want to migrate

@return Return a possible error message
*/
func (s *client) Migrate(tables ...interface{}) error {
	for _, table := range tables {
		mtV := reflect.ValueOf(table).Elem()
		if err := s.Db.AutoMigrate(table); nil != err {
			return errors.New(fmt.Sprint("Migrate", mtV.MethodByName("TableName").Call(nil)[0], "table structure failure---", err.Error()))
		}
	}
	return nil
}

// Insert
/*

@author KangXu

@description Insert some data in batches

@param data is the data to be inserted

@return Returns an error that is the exception information generated by inserting the database
*/
func (s *client) Insert(data ...interface{}) error {
	if 0 < len(data) {
		length := 0
		switch reflect.ValueOf(data[0]).Elem().Kind() {
		case reflect.Slice:
			length = reflect.ValueOf(data[0]).Elem().Len()
		case reflect.Struct:
			length = 1
		}
		return s.Db.Model(data[0]).CreateInBatches(data[0], length).Error
	}
	return errors.New("Null data is not inserted")
}

// Delete
/*

@author KangXu

@description Delete statements encapsulated by mysql

@param table is the structure corresponding to the table name

@param query is the condition for the delete

@return Returns an error that is the exception information generated by deleting the database
*/
func (s *client) Delete(table interface{}, query map[string]interface{}) error {
	keys, values := assembleQueryConditions(query)
	return s.Db.Delete(table, keys, values).Error
}

// Update
/*

@author KangXu

@description Update statements encapsulated by mysql

@param table is the structure corresponding to the table name

@param query is the condition for the update

@param updates is the value of the update

@return Returns an error that is the exception information generated by updating the database
*/
func (s *client) Update(table interface{}, query map[string]interface{}, updates map[string]interface{}) error {
	keys, values := assembleQueryConditions(query)
	return s.Db.Model(table).Where(keys, values...).Updates(updates).Error
}

// SelectOne
/*

@author KangXu

@description Example Query the single data that meets the conditions

@param table is the structure corresponding to the table name

@param result is the object to be returned

@param query is the condition for the update

@return Returns an error that is the exception information generated by querying the database
*/
func (s *client) SelectOne(table interface{}, result interface{}, query map[string]interface{}) error {
	keys, values := assembleQueryConditions(query)
	return ignoreNotFoundErrors(s.Db.Model(table).Take(result, keys, values).Error)
}

// SelectMany
/*

@author KangXu

@description Query multiple pieces of data that match the conditions

@param table is the structure corresponding to the table name

@param result is the object to be returned

@param query is the condition for the update

@return Returns an error that is the exception information generated by querying the database
*/
func (s *client) SelectMany(table interface{}, result interface{}, query map[string]interface{}) error {
	keys, values := assembleQueryConditions(query)
	return ignoreNotFoundErrors(s.Db.Model(table).Find(result, keys, values).Error)
}

// CustomQuery
/*

@author KangXu

@description Supports native sql statements to customize queries

@param result is the result set to be mapped

@param sql is a native sql statement that needs to be executed

@param values is the value to be populated

@return Returns an error that is the exception information generated by querying the database
*/
func (s *client) CustomQuery(result interface{}, sql string, values ...interface{}) error {
	if 0 < len(values) {
		return s.Db.Raw(sql, values...).Scan(result).Error
	}
	return s.Db.Raw(sql).Scan(result).Error
}
